# 2024.06.20——比特Python自学笔记
# 函数

# 函数的概念
# 函数指的是一端可以被重复使用的代码
# 当我们通过Ctrl + C 和 Ctrl + V的方式来重复使用代码时
# 代码完成复制后需要根据不同的数据对代码进行调整
# 一旦复制的代码需要调整，复制了几份就需要调整几次
# 这就会导致如果遗漏了某一次的复制内容，就会使整个程序出现错误
# 因此尽量不要通过复制代码的方式来实现代码的复用
# 这时我们就需要借助函数来实现代码的复用
'''--------------------------------------------------------------------------------------------------------------------'''


# Python中的函数定义格式
# def 函数名(形参列表):
# 缩进 函数体
# return 返回值
# def——define的缩写
# 函数名——命名只能有字母、下划线、数字
# 形参列表——可以有多个形参，形参之间用逗号隔开
# 函数体——函数体中的代码需要带有缩进，
# return——函数的返回值不是必须的，根据函数具体实现的功能来决定

# 在Python中的函数定义格式需要通过关键字def实现函数定义
# 函数的形参不需要指定数据类型
# 函数体以缩进的形式来表示


def func_name(beg, end):  # 无返回值的函数定义
    test = 0
    print(f"test = {test}, beg = {beg}, end = {end}")


def func_name2(beg, end):  # 有返回值的函数定义
    return beg + end


'''--------------------------------------------------------------------------------------------------------------------'''

# 函数调用
# 形式1——无返回值函数调用
# 函数名(实参列表)
# 形式2——有返回值函数调用
# 返回值变量 = 函数名(实参列表）

# 形式1测试
func_name(1, 2)

# 形式2测试
ret = func_name2(1, 2)
print(f"ret = {ret}")

# Python中的函数调用和C/C++/JAVA一样
# 实参列表与形参列表的数量要相同
# 函数的定义只是定义了一个函数，并不会执行函数
# 当对定义的函数进行调用时，函数才能执行，调用几次，执行几次
# 在C/C++中，函数的定义可以放在调用后，但是需要在调用前先声明函数的返回值类型、函数名、形参类型，之后才能正常调用
# 在Python中没有函数声明，因此函数的定义必须在调用前完成

# 测试代码——先声明，后定义
# test(beg, end)
#
#
# def test(beg, end):
#     print(beg, end)
# 在这个代码中程序是会出现报错的，报错的内容为语法错误，不需要等到程序执行就会被程序找出错误

# Python中的警告与错误
# 警告：提示你的代码可能存在问题，但不影响程序编译执行
# 错误：代码中实打实的有问题
# PEP8是Python中非常流行的一套编程规范，类似与C语言中的C99
# 不同的公司使用的编程规范是有差异的，需要根据公司的要求来执行
# 在Python中函数、类定义好之后需要留有两个空行
# 文件的末尾需要留有一个空行
'''--------------------------------------------------------------------------------------------------------------------'''
# 函数参数
# 形参与实参
# 形参是函数定义时同步定义的参数
# 实参是函数调用时同步传入的参数
# 在C/C++/JAVA中由于函数定义的是静态类型，因此函数的实参与形参的类型需要匹配
# 在Python中由于函数定义的是动态类型，因此函数的实参与形参不存在类型匹配的问题
# 在Python中形参的赋值顺序与实参的传参顺序相同
# 在C/C++/JAVA中传参的顺序是从右到左传参，
# Python中的传参需要复合Python的语法规则，如+只能够实现数字相加或者字符串连接
# 如果传入的是数字与字符串相加，则这种传参方式就是有问题的

# 函数传参测试
ret = func_name2(1, 2)
print(f"ret = {ret}")
ret = func_name2(1.5, 2.5)
print(f"ret = {ret}")
ret = func_name2(1, 2.6)
print(f"ret = {ret}")
ret = func_name2('hello ', 'world')
print(f"ret = {ret}")

'''--------------------------------------------------------------------------------------------------------------------'''


# 函数的返回值
# 函数的参数可视为是函数的输入，函数的返回值可视为是函数的输出
# 一个编程的通用原则：一个函数只做一件事
# 如func_name1只进行了打印，func_name2只进行了求和
# 而func_name3中不仅进行了打印，还进行了形参的交换
# 打印属于与用户进行交互，当函数只执行一件事情时，函数的使用会变得很灵活
# 如果一个函数实现了多个功能，就比如这里的打印与交换，此时如果我们想要变更与用户的交互方式
# 很显然func_name3这个函数就不能继续被调用了

# 让逻辑和交互/界面分离进一步的好处就是解耦合
# 一个稍微复杂一些的程序中，经常会涉及到很多个模块，模块之间可能要进行交互，交互就会带来耦合
# 当一个模块发生改变时，耦合强的模块也会同步被改变，耦合弱的模块则不会有所影响
# 在工作中我们会追求编写一些耦合弱的代码

# 一个函数里面可以有多个return语句
# 当执行到return语句是，函数就会立即结束不再执行
# 在Python中的一个函数可以放回多个值，在C/C++中需要通过输出型参数实现（指针/应用），在JAVA中则需要将多个值包装成一个对象，再返回这个对象
# Python中的函数在返回多个值时，可以通过多元赋值的形式来接收函数的返回值，如func_name3
# Golang在设计的时候，也把这个机制给引入过来了
# 当我们返回多个值，但我们只需要其中一部分时，可以通过_来进行占位，如
# def func()
#   return 1, 2, 3
# a, _, _ = func()
# 这样我们就只会获取a的值


def func_name3(beg, end):  # 函数返回值测试
    print(f"beg = {beg}, end = {end}")
    beg, end = end, beg
    print(f"beg = {beg}, end = {end}")
    return beg, end


a = 10
b = 20
a, b = func_name3(a, b)
print(f"a = {a}, b = {b}")
a, _ = func_name3(a, b)
print(f"a = {a}, b = {b}")
'''--------------------------------------------------------------------------------------------------------------------'''

# 变量的作用域
# 函数内部的变量只能在函数内部生效
# 函数外部的变量名可以与函数内部的变量名同名，相互之间互不影响
# Python中也有全局变量与局部变量的概念，在函数内定义的变量称为局部变量，在函数外定义的变量称为全局变量
# 全局变量在整个程序中都有效，局部变量只有在函数内部有效，
# 当全局变量与局部变量同名时，局部变量优先被访问
# 在定义函数时，全局变量的定义需要在函数定义之前
# 关键字global的做用是来声明全局变量
# if,else,while,for这些关键字也会引入“代码块”，但是这些代码块都不会对变量的作用域产生影响
# 在上述语句代码块内部定义的变量可以在外面被访问

tmp1 = 10


def func_name4():  # 变量作用域测试
    print(f"tmp1 = {tmp1}")
    print(f"tmp2 = {tmp2}")
    print(f"tmp3 = {tmp3}")
    print(f"tmp4 = {tmp4}")
    print(f"tmp5 = {tmp5}")


tmp2 = 20

if True:
    tmp3 = 30

while True:
    tmp4 = 40
    break

for tmp5 in range(1, 2):
    pass

func_name4()

# 在Python中只有函数和类才会影响到变量的作用域
'''--------------------------------------------------------------------------------------------------------------------'''

# 函数的执行过程
# 通过调试器来观察函数执行的过程
# 在行首左侧点一下，会出现一个断点
# 调试执行相比于正常的运行最大的区别就是可以随时停下来，方便程序员观察程序的执行过程
# 开始代码调试——>debug (代码调试）——>快捷键Shift + F9
# 代码单步执行——>step into（单步运行）——>快捷键F7
# 代码按行执行——>step over（按行执行）——>快捷键F8
# 结束代码调试——>step out （结束调试）——>快捷键Shift + F8
'''--------------------------------------------------------------------------------------------------------------------'''

# 链式调用
# 用一个函数的返回值作为另一个函数的参数
# 在链式调用中，需要先对括号内的参数求值，即先调用内层函数，再调用外层函数
# 链式调用的时候，也不要嵌套的层次太深，影响可读性

# 链式调用测试
print(func_name2(1, 2))


# 嵌套调用
# 一个函数体内部还能调用其他函数
# 函数嵌套调用的执行过程是由外层函数依次向内进行调用


def a():
    print('函数a')


def b():
    print('函数b')
    a()


# 嵌套调用测试
b()
'''--------------------------------------------------------------------------------------------------------------------'''


# 局部变量与函数栈帧
# 调试器的左下角MainThread窗口可以看到函数之间的调用栈
# 调用栈中描述了当前这个代码的函数之间调用关系
# 每一层这个调用关系就称为函数的栈帧，每个函数的局部变量就在这个栈帧中体现
# 每一层栈帧选中后可以看到里面的局部变量，每个函数的局部变量就保存在对应的栈帧中
# 调用函数，则生成对应的栈帧
# 函数结束，则对应的栈帧消亡
# 不同函数中的同名变量属于不同的函数作用域，存在各自的函数栈帧中


def func_name5():
    a = 20
    b = 30
    print(f"a = {a}、 b = {b}")


a = 10
b = 20
print(f"a = {a}、 b = {b}")
func_name5()
'''--------------------------------------------------------------------------------------------------------------------'''


# 函数递归
# 函数在函数体中调用自己
# 递归代码的两个要素
# 1. 递归结束条件
# 2. 递归的递推公式
# 在求递归的递推公式时，可以根据数学归纳法来进行推导
# 递归的缺点：
# 1. 执行过程非常复杂，难以理解
# 2. 递归代码容易出现栈溢出的问题


def func_name6(n):
    if n == 1:  # 递归结束条件
        return 1
    return n * func_name6(n - 1)  # 递归递推公式


n = 5
print(func_name6(n))

'''--------------------------------------------------------------------------------------------------------------------'''


# Python中的参数默认值
# Python中的函数，可以给形参指定默认值，带有默认值的参数在调用时可以不需要传参
# 这里的参数默认值就是C++中的缺省参数
# 要求：带有默认值的参数，需要在形参列表的后面，而不能在前面/中间


def func_name7(a, b, c: object = True):
    if c:
        print(f'a = {a}, b = {b}')


func_name7(1, 2)
'''--------------------------------------------------------------------------------------------------------------------'''

# 关键字参数
# 按照先后顺序来进行传参的方式称为位置参数
# 按照形参的名字进行传参的方式称为关键字传参
# 通过关键字传参，也可以非常明显的告诉程序员参数需要传给谁，同时可以不用按照形参的顺序进行传参
# 当将位置参数与关键字参数混用时，需要保证位置参数在前，关键字参数在后
# 关键字参数一般搭配默认参数来使用
# 一个函数可以提供很多的参数，来实现这个函数的内部功能做出一些调整设定
# 为了降低调用者的使用成本，就可以把大部分参数设定出默认值
# 当调用者需要调整其中的一部分参数的时候，就可以搭配关键字参数来进行操作


def func_name8(a, b, c=1, d=2, e=3, f=4):    # 关键字参数测试
    print(f'a = {a}, b = {b}')
    print(f'c = {c}, d = {d}')
    print(f'e = {e}, f = {f}')


func_name8(1, 2)
func_name8(3, 4, d = 5, e = 6, f = 7)
func_name8(1, 2, f = 5, e = 4, d = 3)
